﻿using System;
using System.Collections.Generic;
#if DEBUG
using System.Diagnostics;
#endif // #if DEBUG
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using FRL.DebtChased.Extensions;
using FRL.DebtChased.Repository;
using FRL.DebtChased.Silverlight.Library.RichContentService;
using FRL.DebtChased.Silverlight.Library;
using FRL.DebtChased.Types;

namespace FRL.DebtChased
{

	/// <summary>
	/// Declaration of InstalmentCalculator class
	/// Calculates instalment payment schedule, based on start date, frequency etc...
	/// </summary>
	public class InstalmentCalculator
	{

		/////////////////////////////////////////////////////////////
		// Nested class

		/// <summary>
		/// Declaration of Invoice class
		/// Track the paying off of each invoice
		/// </summary>
		internal class Invoice
		{

			/////////////////////////////////////////////////////////
			// Atributes

			public DateTime InvoiceDate { get; set; }

			public Decimal Amount { get; set; }

			public Decimal Interest { get; set; }

		}

		/////////////////////////////////////////////////////////////
		// Atributes

		// List of payments
		private List<AccountPlanPaymentTransport> m_Payments = new List<AccountPlanPaymentTransport>();
		public List<AccountPlanPaymentTransport> Payments
		{
			get { return m_Payments; }
		}

		// List of summary invoice details
		private List<Invoice> m_Invoices = new List<Invoice>();

		// The number of payments
		public int InstalmentCount
		{
			get { return m_Payments.Count; }
		}

		// The total interest added
		private Decimal m_dcTotalInterestAdded = 0;
		public Decimal TotalInterestAdded
		{
			get { return m_dcTotalInterestAdded; }
		}

		// The total payments
		private Decimal m_dcTotalPayments = 0;
		public Decimal TotalPayments
		{
			get { return m_dcTotalPayments; }
		}

		// Total days from plan start to last payment
		public int TotalDays
		{
			get {
				if (m_Payments.Count == 0)
					return 0;
				if (m_frequency == null)
					return 0;
				return (int)Math.Floor( 
					(m_Payments[m_Payments.Count-1].ExpectedAt - m_frequency.Start).TotalDays
				);
			}
		}

		// Member calculator for newtonian approximation of regular payment
		private InstalmentCalculator m_NewtonCalc = null;

		// Is this object a secondary calculator, eg, m_NewtonCalc
		private bool m_bIsSecondaryObject = false;

		// The account that the instalment is calculated against
		private AccountTransport m_account = null;

		// The frequency and start-date for calculations
		private Frequency m_frequency = null;

		// The first payment amount
		private Decimal? m_dcFirstPaymentAmount = null;

		// The interest rate
		private double m_dblInterestRatePerc = 0;

		// The currency of the calculation
		private Money.eCurrency m_currency = Money.eCurrency.NotSet;

		/////////////////////////////////////////////////////////////
		// Construction

		/////////////////////////////////////////////////////////////
		// Operations

		public string Calculate(
			AccountTransport account,
			Frequency frequency,
			decimal? dcFirstPaymentAmount,
			int? iInstalmentCount,
			decimal? dcRegularPayment,
			double dblInterestRatePerc
		)
		{
#if DEBUG
			Debug.Assert((iInstalmentCount != null) || (dcRegularPayment != null));
			Debug.Assert(
				(dcRegularPayment == null)
				||
				(dcRegularPayment.Value.RoundUpPence() == dcRegularPayment.Value)
			);
#endif // #if DEBUG

			// The main function for calculating the payment schedule

			// This function can be called in two ways.
			// Either a regular payment is provided
			// Or a number of instalments is provided
			// The first payment is optional

			// TODO 
			// There is a potential problem with this calculation, in that the interest applied is not only
			// compounded each period, but rounded off to the nearest pence.  This could result in an overpayment
			// or underpayment of up to 0.5p per instalment.

			// Declare return variable
			string strError = null;

			// Declaration of local variables

			// Initialize member variables
			m_account = account;
			m_frequency = frequency;
			m_dcFirstPaymentAmount = dcFirstPaymentAmount;
			m_dblInterestRatePerc = dblInterestRatePerc;

			// Reset the members
			m_Payments.Clear();
			m_Invoices.Clear();
			m_dcTotalInterestAdded = 0;
			m_dcTotalPayments = 0;

			// Get the chasing invoices			
			m_currency = Money.eCurrency.NotSet;
			foreach(AccountInvoiceTransport inv in account.AccountInvoices)
			{

				// If not in query...
				if (!inv.IsInQuery)
				{

					// Add to invoices
					m_Invoices.Add( new Invoice() {
						InvoiceDate = inv.InvoiceDate,
						Amount = inv.Amount.Amount
					});

					// Check currency
					if (m_currency == Money.eCurrency.NotSet)
						m_currency = Money.ParseCurrencyCode(inv.Amount.CurrencyCode);
					else if (m_currency != Money.ParseCurrencyCode(inv.Amount.CurrencyCode))
						m_currency = Money.eCurrency.Mixed;

				}

			}

			// Check for mixed currency, and over payment on first payment
			if (m_currency == Money.eCurrency.Mixed)
				strError = "Instalment planner does not support multiple currency accounts.";
			else if (
				(dcFirstPaymentAmount != null) 
				&& 
				(dcFirstPaymentAmount > _calcRemainingTotal())
			)
				strError = "The first payment is larger than the chasing balance.";

			// If that worked...
			if (strError == null)
			{

				// Sort invoices into oldest first, as the payments will be applied to the oldest first
				for( int i1=0; i1<m_Invoices.Count; i1++ )
				{
					bool bDelta = false;
					for( int i2 =0; i2<m_Invoices.Count-1; i2++ )
					{
						if(m_Invoices[i2].InvoiceDate > m_Invoices[i2+1].InvoiceDate)
						{
							bDelta = true;
							Invoice Temp = m_Invoices[i2];
							m_Invoices.RemoveAt(i2);
							m_Invoices.Insert(i2,Temp);
						}
					}
					if(!bDelta)
						break;
				}

			}

			// If that worked...
			if(strError == null)
			{

				// If adding regular payments...
				if (dcRegularPayment != null)
				{

					// apply regular payment
					strError = _applyRegularPayment(dcRegularPayment.Value, iInstalmentCount);

				}
				else
				{
#if DEBUG
					Debug.Assert(!m_bIsSecondaryObject);
#endif // #if DEBUG

					// Need to calculate a "sensible" regular payment to fit the repayment into
					// a predefined number of instalments.

					// Loop, calculating different payments until plan fits timeframe
					decimal dcMinRegularPayment = 0.01M;
					decimal dcMaxRegularPayment = _calcRemainingTotal() * 2;
					if (_calcRemainingBalance(dcMinRegularPayment, iInstalmentCount.Value) <= 0)
					{

						// The payment of 1p is too large to pay off the debt in the 
						// desited number of instalments.

						// Update error
						strError = "Failed to calculate regular payments.";

					}
					else
					{
#if DEBUG
						Debug.Assert(_calcRemainingBalance(dcMinRegularPayment, iInstalmentCount.Value) > 0);
						Debug.Assert(_calcRemainingBalance(dcMaxRegularPayment, iInstalmentCount.Value) < 0);
#endif // #if DEBUG

						// Use newtonian approximation to find a repayment amount
						int iErrorCount = 0; // If more than 100 iterations, assume something has gone wrong
						while (true)
						{

							// Pick half-way value between min and max
							decimal dcTestRegularPayment = dcMinRegularPayment + (dcMaxRegularPayment - dcMinRegularPayment) / 2;

							// Round up to nearest whole pence
							dcTestRegularPayment = dcTestRegularPayment.RoundUpPence();

							// Apply this payment and return the difference
							decimal dcDeltaBalance = _calcRemainingBalance(dcTestRegularPayment, iInstalmentCount.Value);

							// A negative dcDeltaBalance means too much has been paid off
							// A positive dcDeltaBalance means not enough has been paid off

							// If the final balance is an overpayment between 0 and 10... near enough!
							if ((dcDeltaBalance < 0) && (dcDeltaBalance > -10))
							{

								// Try to convert regular payment to "sensible" amount, for example, 99.98 -> 100

								// Can this figure be floored? (Can the pence be chopped off?)

								// Need to test if flooring off pence is a good idea. For large numbers of installments
								// a few pence can result in a non-"sensible" final payment.

								// If the difference after chopping pence off is less than half the regular payment...
								decimal dcDifference = iInstalmentCount.Value * (dcTestRegularPayment % 1);
								if (dcDifference <= (dcTestRegularPayment / 2))
								{

									// floor the pence
									dcTestRegularPayment = (decimal)Math.Floor((double)dcTestRegularPayment);

									// Test rounding off of the integer, so 99 ->100 and 201 -> 200
									decimal dcMod = dcTestRegularPayment % 10;
									if (dcMod == 1)
									{

										// Test for rounding down
										decimal dcTestPayment = dcTestRegularPayment - 1;
										decimal dcFinalBalance2 = _calcRemainingBalance(dcTestPayment, iInstalmentCount.Value);
										if (dcFinalBalance2 < 0)
											dcTestRegularPayment = dcTestPayment;

									}
									else if (dcMod == 9)
									{

										// Test for rounding up
										decimal dcTestPayment = dcTestRegularPayment + 1;
										decimal dcFinalBalance2 = _calcRemainingBalance(dcTestPayment, iInstalmentCount.Value);
										if (dcFinalBalance2 < 0)
											dcTestRegularPayment = dcTestPayment;

									}

								}

								// Apply the neater value (restrict to number of instalments)
								_applyRegularPayment(dcTestRegularPayment, iInstalmentCount.Value);

								// Break out of loop
								break;

							}
							else
							{

								// Correct the payment, pivoting between 0 and -10
								if (dcDeltaBalance < -5)
									dcMaxRegularPayment = dcTestRegularPayment;
								else
									dcMinRegularPayment = dcTestRegularPayment;

							}

							// Move on
							iErrorCount++;
							if (iErrorCount == 100)
							{
								strError = "Failed to estimate regular payment for fixed Instalments.";
								break;
							}

						} // while

					}

				}

			}
			
			// Return variable
			return strError;

		}

		private string _applyRegularPayment(decimal dcRegularPayment, int? iInstalmentCount)
		{
#if DEBUG
			Debug.Assert(
				((m_bIsSecondaryObject) && (iInstalmentCount != null))
				||
				(!m_bIsSecondaryObject)
			);
#endif // #if DEBUG

			// Apply a regular payments until debot paid off, or if iInstalmentCount
			// provided, for a set number of installments

			// Note that if this is a secondary object, keep paying off for all instalements
			// regardless of overpayment, just to see how much the account is overpaid

			// Declare return variable
			string strError = null;

			// Loop through payments
			int iPaymentOffset = 0;
			DateTime dtLastPaymentDate = DateTime.Now;
			int iError = 0;
			while (true)
			{

				// Check the remaining total
				decimal dcRemainingTotal = _calcRemainingTotal();
#if DEBUG
				Debug.Assert(dcRemainingTotal.RoundUpPence() == dcRemainingTotal);
#endif // #if DEBUG
				if ((!m_bIsSecondaryObject) && (dcRemainingTotal <= 0))
				{
#if DEBUG
					Debug.Assert(dcRemainingTotal == 0);
#endif // #if DEBUG
					break;
				}

				// Get payment date
				DateTime dtPayment = m_frequency.CalculateNthPayment(iPaymentOffset);

				// Add interest and payment to the invoices
				foreach (Invoice inv in m_Invoices)
				{

					// If this is the first payment...
					decimal dcDeltaInterest = 0;
					if (iPaymentOffset == 0)
						dcDeltaInterest = _calcInterest(inv.Amount + inv.Interest, inv.InvoiceDate, dtPayment);
					else
						dcDeltaInterest = _calcInterest(inv.Amount + inv.Interest, dtLastPaymentDate, dtPayment);
					dcDeltaInterest = dcDeltaInterest.RoundUpPence();
					inv.Interest += dcDeltaInterest;
					m_dcTotalInterestAdded += dcDeltaInterest;

				}

				// Calculate this payment
				decimal dcThisPayment = dcRegularPayment;
				if ((iPaymentOffset == 0) && (m_dcFirstPaymentAmount != null))
				{

					// The first payment
					dcThisPayment = m_dcFirstPaymentAmount.Value;

				}
				else if ((!m_bIsSecondaryObject) && (iInstalmentCount != null) && (iPaymentOffset == (iInstalmentCount - 1)))
				{

					// Mop up any extra pence in the final payment
					dcThisPayment = Math.Max(dcThisPayment, _calcRemainingTotal());

				}
				if (!m_bIsSecondaryObject)
					dcThisPayment = Math.Min(dcThisPayment, _calcRemainingTotal());
#if DEBUG
				Debug.Assert(dcThisPayment == dcThisPayment.RoundUpPence());
#endif // #if DEBUG

				// Apply the payment
				m_dcTotalPayments += dcThisPayment;
				_addPayment(dcThisPayment, dtPayment);

				// Move on
				dtLastPaymentDate = dtPayment;
				iError++;
				if (iError > 100)
				{
					strError = "This plan would require more than 100 payments.";
					break;
				}
				iPaymentOffset++;
				if (
					(m_bIsSecondaryObject)
					&&
					(iInstalmentCount != null)
					&&
					(iPaymentOffset == iInstalmentCount.Value)
				)
					break;

			} // while-loop

			// Return variable
			return strError;

		}

		private decimal _calcRemainingBalance( decimal dcTestRegularPayment, int iInstalmentCount )
		{

			// Declare calculator
			if (m_NewtonCalc == null)
			{
				m_NewtonCalc = new InstalmentCalculator();
				m_NewtonCalc.m_bIsSecondaryObject = true;
			}
#if DEBUG
			else
				Debug.Assert(m_NewtonCalc.m_bIsSecondaryObject);
#endif // #if DEBUG

			// Calculate result of payments
			m_NewtonCalc.Calculate(
				m_account,
				m_frequency,
				m_dcFirstPaymentAmount,
				iInstalmentCount,
				dcTestRegularPayment,
				m_dblInterestRatePerc
			);

			// Return the remaining balance
			return m_NewtonCalc._calcRemainingTotal();

		}

		private decimal _calcRemainingTotal()
		{

			// Declare return variable
			decimal dcFtmp = 0;

			// Loop through invoices
			foreach (Invoice inv in m_Invoices)
			{
				dcFtmp += inv.Amount;
				dcFtmp += inv.Interest;
			}

			// Return variable
			return dcFtmp;

		}

		private void _addPayment(decimal dcAmount, DateTime dtStart)
		{
#if DEBUG
			Debug.Assert(dcAmount.RoundUpPence() == dcAmount);
#endif // #if DEBUG

			// Subtract an amount from the invoices, starting with the oldest

			// Add payment to list
			m_Payments.Add(new AccountPlanPaymentTransport() {
				Amount = (decimal)dcAmount,
				Currency = Money.CurrencyToCode(m_currency),
				ExpectedAt = dtStart
			});

			// Remove the amount from the oldest invoice
			foreach (Invoice inv in m_Invoices)
			{

				// If there is an amount to remove
				if (inv.Amount > 0)
				{
					if (inv.Amount >= dcAmount)
					{
						inv.Amount -= dcAmount;
						dcAmount = 0;
					}
					else
					{
						dcAmount -= inv.Amount;
						inv.Amount = 0;
					}
				}
				if (dcAmount == 0)
					break;

				// If there is interest to remove...
				if (inv.Interest > 0)
				{
					if (inv.Interest >= dcAmount)
					{
						inv.Interest -= dcAmount;
						dcAmount = 0;
					}
					else
					{
						dcAmount -= inv.Interest;
						inv.Interest = 0;
					}
				}
				if (dcAmount == 0)
					break;

			} // foreach-loop

			// If in internal mode...
			if (m_bIsSecondaryObject)
			{

				// Make sure the whole payment is used up
				if(dcAmount > 0)
					m_Invoices[0].Amount -= dcAmount;

			}
#if DEBUG
			else
				Debug.Assert(dcAmount == 0);
#endif // #if DEBUG

		}

		private decimal _calcInterest(
			decimal dcAmount,
			DateTime date1,
			DateTime date2
		)
		{

			// Declare return variable
			decimal dcFtmp = 0;

			// Calculate the days
			double dblDays = Math.Floor((date2 - date1).TotalDays);
			if (dblDays < 0)
			{
#if DEBUG
				Debug.Assert(false);
#endif // #if DEBUG
				dblDays = 0;
			}
			if ( (dblDays > 0) && (dcAmount>0) )
			{
				double dblTemp = (double)dcAmount;
				dblTemp = (dblTemp * m_dblInterestRatePerc * dblDays / (365.0 * 100.0));
				dcFtmp = (decimal)dblTemp.RoundUpPence();
			}

			// Return variable
			return dcFtmp;

		}

	}

}
